'''
All the basic rules that can be applied to any robot test. 
'''
import os
import robot.parsing.model
import robot.parsing.settings
from string import Template
from rsa.report import ReportManager
from rsa.rule import Rule, TemplateRule, RuleViolation
from rsa.util import AUTOMATICVARS, GLOBALVARS
from rsa.util.robot_utilities import find_keyword, imported_resources, get_full_name
from rsa.util.common_utilities import get_variables, clean, is_constant, is_variable, violation_reported

name = "Basic"

description = __doc__

report = ReportManager()

class SetGlobalVariables(TemplateRule):
    """
    """
    
    rule_id = "SetGlobalVariables"
    name = "Set global variables"
    example = '''
    '''
    severity = 2
    description = ""
    set_variable_keyword = ["Set Global Variable", "Set Suite Variable", "Set Test Variable", "Set Environment Variable"]
               
    def visit_step(self, node):
        if(node.keyword in self.set_variable_keyword):
            self.save_variable(node.keyword, node.args[0])
        if(node.keyword == "Run Keyword If"):
            if(node.args[1] in self.set_variable_keyword):
                self.save_variable(node.args[1], node.args[2])

    def visit_loop(self, node):
        for step in node.steps:
            self.line += 1
            self.visit(step)
            
    def save_variable(self, keyword, arg):
        gvar = arg.replace("\\", "").replace(" ", "").lower()
        if(not (keyword == "Set Environment Variable")):
            gvar = gvar[2:-1]
        if(not (gvar in GLOBALVARS)):
            GLOBALVARS.append(gvar)
                        
class InvalidTestName(Rule):
    """
    This rule checks that every test case name doens't contain an invalid character (".","%") 
    """
    
    rule_id = "InvalidTestName"
    name = "Invalid test name"
    message = Template("Character '$item' is not allowed in the test name")
    solution = Template("Remove character '$item'")
    example = '''
            #This is an invalid test name: Booking%Car
    '''
    severity = 2
    description = "InvalidTestName find instances where a test name contains one or more invalid characters"
    
    def __init__(self):
        self.invalid_characters = ["?", "%", "^", "."]
       
    def add_invalid_character(self, param):
        if(param == None):
            raise Exception("Null Rule reference added to a invalid test name parameter; that's a bug!")
        self.invalid_characters.append(param)
    
    def apply(self, node, symbols):
        name = node.name
        for invalid in self.invalid_characters:
            if(name.find(invalid) != -1):
                report.add_rule_violation(RuleViolation(self, None, invalid, node))
                
class UndefinedVariable(TemplateRule):
    """
    This rule checks if a used variable was previously declared 
    """

    rule_id = "UndefinedVariable"
    name = "Undefined variable"
    message = Template("'$item' cannot be resolved")
    solution = Template("Create local variable '$item'")
    example = '''
            # ${locator1} is not defined
            ${var1} = | Get Text | ${locator1}
    '''
    severity = 0
    description = "UndefinedVariable find instances where an undefined variable is called."
    set_variable_keyword = ["Set Global Variable", "Set Suite Variable", "Set Test Variable", "Set Environment Variable"]
    
    def visit_step(self, node):
        if(len(node.assign) > 0):
            for assign in node.assign:
                self.testvars.append(clean(assign)[2:-1])
        for arg in node.args:
            if(arg.find('${') != -1 or arg.find('@{') != -1 or arg.find('%{') != -1):
                vars = get_variables(clean(arg, False))
                for var in vars:
                    if(self.is_defined(var) == False):
                        report.add_rule_violation(RuleViolation(self, node, var, self.node, self.line))
                    
    def visit_loop(self, node):
        for var in node.vars:
            #self.testvars.append(var)
            self.testvars.append(var[2:-1])
        for item in node.items:
            if(is_variable(item) and item[2:-1].isdigit() == False and self.is_defined(item[2:-1]) == False):
                report.add_rule_violation(RuleViolation(self, node, item[2:-1], self.node, self.line))
        for step in node.steps:
            self.line += 1
            self.visit(step)
                    
    def is_defined(self, var):
        exvar = []
        exvar.extend(GLOBALVARS)
        exvar.extend(AUTOMATICVARS)
        for avar in exvar:
            if(var.lower() == avar.lower()):
                return True
        # check for variable in test variables
        for testvar in self.testvars:
            if(testvar.lower() == var.lower()):
                return True
        # check for variable in global variables
        suite = get_full_name(self.node)
        if(self.symbols.get_variable(var, suite[:suite.rfind(".")])):
            return True
        return is_constant(var)
    
    def start(self):
        self.testvars = []
        # check for arguments and return value variables
        if(isinstance(self.node, robot.parsing.model.UserKeyword)):
            for value in self.node.args.value:
                self.testvars.append(clean(value)[2:-1].replace(" ", ""))
                
class UnusedVariable(TemplateRule):
    """
    This rule searches for variables defined but never used.
    """
    
    rule_id = "UnusedVariable"
    name = "Unused variable"
    message = Template("The local variable '$item' is never read")
    solution = Template("Remove '$item' keep side-effects assignments")
    example = '''
            # ${var1} is declared but not used
            ${var1} = | Get Text | ${locator1}
            ${var2} = | Get Text | ${locator2}
            Should Be Equal As Strings | ${var2} | Still Life with Woodpecker
    '''
    severity = 1
    description = "UnusedVariable find instances where a variable is declared but not used."
                        
    def visit_step(self, node):
        if len(node.assign) > 0: 
            for assign in node.assign:
                if not is_constant(assign):
                    self.declaredline.append(self.line)
                    var_value = clean(assign)[2:-1].lower()
                    self.declaredvars.append(var_value)
        for arg in node.args:
            if(arg.find('${') != -1 or arg.find('@{') != -1 or arg.find('%{') != -1):
                vars = get_variables(clean(arg, False))
                for uvar in vars:
                    var_value = uvar.lower()
                    if var_value not in self.usedvars:
                        self.usedvars.append(var_value)
                
    def visit_loop(self, node):
        for var in node.vars:
            var_value = var[2:-1].lower()
            self.declaredvars.append(var_value)
            self.declaredline.append(self.line)
        for item in node.items:
            if(item.isdigit() == False):
                var_value = clean(item)[2:-1].lower()
                if not var_value in self.usedvars:
                    self.usedvars.append(var_value)
        for step in node.steps:
            self.line += 1
            self.visit(step)
            
    def start(self):
        self.usedvars = []
        self.declaredvars = []
        self.declaredline = []
        # check for return value and arguments variables
        if(isinstance(self.node, robot.parsing.model.UserKeyword)):
            for value in self.node.args.value:
                self.declaredline.append(0)
                var_value = clean(value)[2:-1].lower() 
                self.declaredvars.append(var_value)
            for value in self.node.return_.value:
                var_value = clean(value)[2:-1].lower()
                if var_value.find("[") != -1 and var_value.find("]") != -1:
                    var_value = var_value[:var_value.find("[")] + var_value[var_value.rfind("]")+1:]
                self.usedvars.append(var_value)
        
    def end(self):
        for i in range(self.declaredvars.__len__()):
            found = False
            for used in self.usedvars:
                if(self.declaredvars[i] == used):
                    found = True
            if(found == False):
                report.add_rule_violation(RuleViolation(self, None, self.declaredvars[i], self.node, self.declaredline[i]))
                
class InvalidVariableAssignment(TemplateRule):
    """
    This rule verifies that every variable assigment contains the operator '='  
    """
    
    rule_id = "InvalidVariableAssignment"
    name = "Invalid variable assignment"
    message = Template("Operator '=' is missing.")
    solution = Template("Insert '=' after $item")
    example = '''
            # Invalid assignment, = operator missing
            ${var1} | Get Text | //input[@rule_id='HBSI']  
    '''
    severity = 1
    description = "InvalidVariableAssignment find instances where an assignment is made without '=' operator"     
           
    def visit_step(self, node):
        if(len(node.assign) == 1):
            var = node.assign[0]
            if(is_constant(var)):
                if(var != "${EMPTY}"):
                    report.add_rule_violation(RuleViolation(self, node, var, self.node, self.line))
            elif(var.find("=") == -1):
                if(node.keyword != "Set Variable" and node.keyword != "Get File"):
                    report.add_rule_violation(RuleViolation(self, node, var, self.node, self.line))
                
    def visit_loop(self, node):
        for step in node.steps:
            self.line += 1
            self.visit(step)
            
class GetLengthOfList(Rule):
    """
    This rule checks that the keyword GetLength (of a list) is  
    """
    
    rule_id = "GetLengthOfList"
    name = "Get length of list"
    message = Template("Argument $item should be a variable.")
    solution = Template("Remove $item, use a variable instead (not a list)")
    example = '''
            @{properties} = | Create List | 46936 | 46937
            # this is a valid call
            ${count} = | Get Length | ${properties}
            # this is a invalid call      
            ${count} = | Get Length | @{properties}
    '''
    severity = 0
    description = "GetLengthOfList find instances where the method Get Length is not used correctly"
       
    def apply(self, node, symbols):
        for i in range(node.steps.__len__()):
            step = node.steps[i]
            if(step.__class__.__name__ == 'Step'):
                if(step.keyword == "Get Length"):
                    if(len(step.args) == 1):
                        if(step.args[0].find("@{") != -1):
                            report.add_rule_violation(RuleViolation(self, step, step.args[0], node, i))

class UserKeywordArguments(TemplateRule):
    """
    This rule verifies that a user keyword is invoked with the correct amount of parameters
    """
    
    rule_id = "UserKeywordArguments"
    name = "User keyword arguments"
    message = Template("Keyword '$item1' expected $item3 argument, got $item2")
    solution = Template("Remove arguments to match $item3")
    example = '''
            # LogIn keyword accepts only two arguments      
            LogIn | ${username} | ${password} | ${invalid}
    '''
    severity = 0
    description = "UserKeywordArguments find instances where a user keyword is called with the wrong number of arguments"

    def visit_step(self, node):
        references = self.symbols.find_keyword(node.keyword)
        if(len(references) > 0):
            keyword = find_keyword(node.keyword, self.node.parent.parent)
            if(keyword != None):
                args1 = len(node.args)
                args2 = len(keyword.args.value)
                if(args1 != args2):
                    min = self.get_min_args(keyword.args.value)
                    if(not(min <= args1 <= args2)):
                        args = [keyword.name, str(args1), str(args2)]
                        report.add_rule_violation(RuleViolation(self, node, args, self.node, self.line))
                
    def visit_loop(self, node):
        for step in node.steps:
            self.line += 1
            self.visit(step)
            
    def get_min_args(self, args):
        min = len(args)
        for arg in args:
            if(arg.find("=") != -1):
                min -= 1
        return min
    
class EmptyLine(TemplateRule):
    """
    This rule verifies that a test cases doesn't contains empty lines
    """
    
    rule_id = "EmptyLine"
    name = "Empty line"
    message = Template("$item line found")
    solution = Template("Remove this line")
    example = '''
            # Unnecessary line      
            ${EMPTY} |
    '''
    severity = 2
    description = "EmptyLine find instances where a test case has an empty line"
    
    def visit_step(self, node):
        if(len(node.assign) == 1 and node.keyword == None):
            if(node.assign[0] == "${EMPTY}"):
                report.add_rule_violation(RuleViolation(self, node, "${EMPTY}", self.node, self.line))
                
    def visit_loop(self, node):
        for step in node.steps:
            self.line += 1
            self.visit(step)
            
class UnnecessaryImport(Rule):
    """
    This rule verifies that a suites doesn't import resources that are already imported via other resources 
    """
    
    rule_id = "UnnecessaryImport"
    name = "Unnecessary import"
    message = Template("Resource '$item' is already imported")
    solution = Template("Remove '$item'")
    example = '''
            # Avoid this to clean your imports
            Hotel Operations
                \ import OperatingSystem
                * Inventory Change Log
                    \ import OperatingSystem
                    - HP_ICL_0001_LayoutVerification
                * Hotel Change Log
                    - HP_ICL_0001_LayoutVerification
    '''
    severity = 1
    description = "UnnecessaryImport find instances where a resource was imported more than once"
    
    def apply(self, node, symbols):
        if(violation_reported(report.report, self.name, node.parent.parent)): return
        imports = []
        imported_resources(node, symbols, imports)
        for imp1 in node.parent.parent.imports:
            for imp2 in imports:
                if(isinstance(imp2, robot.parsing.settings.Library)): continue
                if((imp1.type == imp2.type) and (clean(imp1.name) == clean(imp2.name))
                   and self.same_arguments(imp1, imp2)):
                    if(isinstance(imp2.parent.parent, robot.parsing.model.ResourceFile)):
                        where = imp1.parent.parent.name + ": " + imp1.name
                        if(not violation_reported(report.report, self.name, node.parent.parent, where)):
                            report.add_rule_violation(RuleViolation(self, None, imp1.name, node.parent.parent))
                    if(isinstance(imp2.parent.parent, robot.parsing.model.TestDataDirectory)):
                        if(self.has_set_up_keywords(imp2.parent.parent)):
                            break
                        #where = imp2.parent.parent.name + ": " + imp2.name
                        where = imp2.name
                        if(not violation_reported(report.report, self.name, imp2.parent.parent, where)):
                            report.add_rule_violation(RuleViolation(self, None, imp2.name, imp2.parent.parent))

    def has_set_up_keywords(self, suite):
        if(suite.setting_table.suite_setup.name != None or suite.setting_table.suite_teardown.name != None or 
           suite.setting_table.test_setup.name != None or suite.setting_table.test_teardown.name != None):
            return True
        return False

    def same_arguments(self, imp1, imp2):
        if(len(imp1.args) == len(imp2.args)):
            for i in range(1, len(imp1.args)):
                if(imp1.args[i] != imp2.args[i]):
                    return False
            return True
        return False
    
class InvalidTestLength(Rule):
    """
    This rule verifies that a test name is not greater than the maximum allowed, and the same for the number of steps
    """
    
    rule_id = "InvalidTestLength"
    name = "Invalid test length"
    message = Template("$item1 ($item2)")
    solution = Template("Restrict the $item3 to $item4")
    example = '''
    '''
    severity = 2
    description = "InvalidTestLength find instances where a test case violates the maximum number of steps"
    name_threshold = 128
    steps_threshold = 128
    
    def apply(self, node, symbols):
        if(len(node.name) > self.name_threshold):
            args = ["Test name too long", str(len(node.name)), "characters", str(self.name_threshold)]
            report.add_rule_violation(RuleViolation(self, None, args, node))
        if(len(node.steps) > self.steps_threshold):
            args = ["To many steps", str(len(node.name)), "steps", str(self.steps_threshold)]
            report.add_rule_violation(RuleViolation(self, None, args, node))
                     
class NoAbsolutePathNames(Rule):
    """
    This rule verifies that a suite doesn't import resources with absolute path names 
    """
    
    rule_id = "NoAbsolutePathNames"
    name = "No absolute path names"
    message = Template("No absolute path names allowed '$item'")
    solution = Template("Change '$item' to a relative path")
    example = '''
            \ import C:/Somebody/OperatingSystem
            * Inventory Change Log
                \ import OperatingSystem
                - HP_ICL_0001_LayoutVerification
    '''
    severity = 0
    description = "NoAbsolutePathNames find resources that contains absolute path names"
    
    def apply(self, node, symbols):
        for imp in node.parent.parent.imports:
            if(isinstance(imp, robot.parsing.settings.Resource)):
                abspath = os.path.abspath(imp.name)
                if(abspath == imp.name):
                    if(not violation_reported(report.report, self.name, imp.parent.parent, abspath)):
                            report.add_rule_violation(RuleViolation(self, None, abspath, imp.parent.parent))
